﻿using System;
using SoftwareConsulting.BI11.NumericalMethods.Interfaces;
using SoftwareConsulting.BI11.NumericalMethods.Statistics;
using SoftwareConsulting.BI11.NumericalMethods.Regression;
using SoftwareConsulting.BI11.NumericalMethods.Approximation;

namespace SoftwareConsulting.BI11.NumericalMethods.Curves
{    
    /// <summary>
    /// Класс гистограммы (обозначает частоту попадания в заданный интервал)
    /// </summary>
    public class Histogram : IPointsSeries, IHistogram
    {	    
        private double _minimum;//нижняя граница первого интервала гистограммы
        private double _binWidth;//ширина интервала
        private int[] _contents;//содержимое гистограммы
        private bool _growthAllowed = false;//флаг автоматического расширения
        private bool _integerBinWidth = false;//флаг целочисленной ширины интервала	    
        private int _underflow;//число значений, расположенных ниже первого интервала (также используется для расчета закэшированных значений, когда включен кэш)
        private int _overflow;//число значений, расположенных выше последнего интервала (также используется для хранения желаемого числа интервалов, когда включено кэширование)
        private StatisticalMomentsFixed _moments;//статистические моменты значений, накапливаемых в границах гистограммы
        private bool _cached = false;//флаг кэширования значений гистограммы для вычисления достаточного интервала
        private double[] _cache;//кэш хранимых значений

        /// <summary>
        /// Конструктор класса с незаданными границами и 50ю интервалами. 
        /// Первые 100 значений кэшируются, затем вычисляется подходящий диапазон.        
        /// </summary>
        public Histogram():
            this( 100)
        {
        }

        /// <summary>
        /// Конструктор класса для приближения диапазона желаемых 50ти интервалов.
        /// Все параметры выравниваются так, чтобы ширина интервала была округленным числом.        
        /// </summary>
        public Histogram(double from, double to) : 
            this( from, to, 50)
        {
        }

        /// <summary>
        /// Конструктор класса для приближения диапазона желаемого числа интервалов.
        /// Все параметры выравниваются так, чтобы ширина интервала была округленным числом.
        /// </summary>
        public Histogram(double from, double to, int binsNumber)
        {
            DefineParameters(from, to, binsNumber);
        }

        /// <summary>
        /// Конструктор класса с незаданными границами и 50ю интервалами. 
        /// Накапливаемые значения кэшируются, а когда кэш полон, вычисляется подходящий диапазон.        
        /// </summary>        
        public Histogram(int cacheSize) : 
            this(cacheSize, 50)
        {
        }

        /// <summary>
        /// Конструктор класса        
        ///	<remarks>кидает ArgumentOutOfRangeException, если
        ///число интервалов меньше или равно нулю,
        ///или если границы заданы в неверном порядке.</remarks>
        /// </summary>
        public Histogram(int binsNumber, double min, double binWidth)
        {
            if (binWidth <= 0)
                throw new ArgumentOutOfRangeException("binWidth");                                            
            _contents = new int[binsNumber];
            _minimum = min;
            _binWidth = binWidth;
            Reset();
        }

        /// <summary>
        /// Конструктор класса с незаданными границами. 
        /// Накапливаемые значения кэшируются, а когда кэш полон, вычисляется подходящий диапазон.        
        /// </summary>        
        public Histogram(int cacheSize, int binsNumber)
        {
            _cached = true;
            _cache = new double[cacheSize];
            _underflow = 0;
            _overflow = binsNumber;
        }

        /// <summary>
        /// Накапливает случайную переменную. 
        /// </summary>
        /// <param name="randomVariableValue"></param>       
        public void Accumulate(double randomVariableValue)
        {
            if (_cached)
            {
                _cache[_underflow++] = randomVariableValue;
                if (_underflow == _cache.Length)
                    FlushCache();
                return;
            }            
            if (randomVariableValue < _minimum)
            {
                if (_growthAllowed)
                {
                    ExpandDown(randomVariableValue);
                    _moments.Accumulate(randomVariableValue);
                }
                else
                    _underflow++;
                return;
            }            
            int index = BinIndex(randomVariableValue);
            if (index < _contents.Length)
            {
                _contents[index]++;
                _moments.Accumulate(randomVariableValue);
            }
            else if (_growthAllowed)
            {
                ExpandUp(randomVariableValue);
                _moments.Accumulate(randomVariableValue);
            }
            else
                _overflow++;            
        }

        /// <summary>
        /// Возвращает среднюю величину значений, накопленных в интервалах гистограммы
        /// </summary>
        public double Average
        {
            get
            {
                if (_cached)
                    FlushCache();
                return _moments.Average;
            }
        }

        /// <summary>
        /// Возвращает позицию интервала, в котором расположено указанное число
        /// </summary>
        /// <param name="needValue"></param>
        /// <returns></returns>
        public int BinIndex(double needValue)
        {
            return (int)Math.Floor((needValue - _minimum) / _binWidth);
        }
        
        /// <summary>
        /// Возвращает значение противодействия Chi2 гистограммы относительно указанного распределения вероятности
        /// </summary>			        
        public double Chi2Against(ScaledProbabilityDensityFunction probabilityDensityFunction)
        {
            double chi2 = 0;
            for (int i = 0; i < _contents.Length; i++)
                chi2 += new WeightedPoint(1, _contents[i]).GetChi2Contribution(probabilityDensityFunction);
            return chi2;
        }

        /// <summary>
        /// Возвращает значение уровня доверия Chi2 гистограммы относительно указанного распределения вероятности
        /// </summary>
        public double Chi2ConfidenceLevel(ScaledProbabilityDensityFunction probabilityDensityFunction)
        {
            return (new ChiSquareDistribution(
                _contents.Length - probabilityDensityFunction.Parameters.Length)
                ).GetConfidenceLevel(Chi2Against(probabilityDensityFunction));
        }

        /// <summary>
        /// Возвращает число накопленных значений
        /// </summary>
        public int BinsCount
        {
            get 
            { 
                return _cached ? _underflow : _moments.Count; 
            }
        }
        
        /// <summary>
        /// Вычисляет подходящие границы и ширину интервала.
        /// </summary>
        ///	<remarks>кидает ArgumentOutOfRangeException, если
        ///число интервалов меньше или равно нулю,
        ///или если границы заданы в неверном порядке.</remarks>
        private void DefineParameters(double from, double to, int binsNumber)
        {
            if (from >= to)
                throw new ArgumentOutOfRangeException("to");
            if (binsNumber < 1)
                throw new ArgumentOutOfRangeException("binsNumber");
            _binWidth = MathExtended.RoundToScale((to - from) / binsNumber, _integerBinWidth);
            _minimum = _binWidth * Math.Floor(from / _binWidth);
            int numberOfBins = (int)Math.Ceiling((to - _minimum) / _binWidth);
            if (_minimum + numberOfBins * _binWidth <= to)
                numberOfBins++;
            _contents = new int[numberOfBins];
            _cached = false;
            _cache = null;
            Reset();
        }

        /// <summary>
        /// Возваращет ошибку среднего
        /// </summary>
        /// <remarks>Может кидать исключение "деление на ноль"</remarks>
        public double GetErrorOnAverage()
        {
            if (_cached)
                FlushCache();
            return _moments.ErrorOnAverage;            
        }

        /// <summary>
        /// Расширает содержимое так, чтобы нижний интервал включал указанное значение
        /// </summary>        
        private void ExpandDown(double valueToInclude)
        {
            int addSize = (int)Math.Ceiling((_minimum - valueToInclude) / _binWidth);
            int[] newContents = new int[addSize + _contents.Length];
            _minimum -= addSize * _binWidth;
            newContents[0] = 1;
            Array.Clear(newContents, 1, addSize - 1);
            for (int n = 0; n < _contents.Length; n++)
                newContents[n + addSize] = _contents[n];
            _contents = newContents;
        }

        /// <summary>
        /// Расширает содержимое так, чтобы верхний интервал включал указанное значение
        /// </summary>      
        private void ExpandUp(double valueToInclude)
        {
            int newSize = (int)Math.Ceiling((valueToInclude - _minimum) / _binWidth);
            int[] newContents = new int[newSize];
            int n;
            for (n = 0; n < _contents.Length; n++)
                newContents[n] = _contents[n];
            Array.Clear(newContents, _contents.Length, newSize - 1 - _contents.Length);
            newContents[newSize - 1] = 1;
            _contents = newContents;
        }

        /// Возвращает уровень доверия по F-тесту с данными, накопленными в указанной гистограмме        
        public double GetFConfidenceLevel(Histogram h)
        {
            return _moments.GetFConfidenceLevel(h._moments);
        }

        /// <summary>
        /// Очищает кэш
        /// </summary>
        private void FlushCache()
        {
            double min = _cache[0];
            double max = min;
            int cacheSize = _underflow;
            double[] cachedValues = _cache;
            for (int n = 1; n < cacheSize; n++)
            {
                if (_cache[n] < min)
                {
                    min = _cache[n];
                    continue;
                }
                if (_cache[n] > max)
                    max = _cache[n];
            }
            DefineParameters(min, max, _overflow);
            for (int n = 0; n < cacheSize; n++)
                Accumulate(cachedValues[n]);
        }

        /// <summary>
        /// Возвращает содержимое указанного значения гистограммы
        /// </summary>
        /// <param name="x"></param>
        /// <returns></returns>
        public double GetValueByX(double x)
        {
            if (x < _minimum)
                return double.NaN;
            int n = BinIndex(x);
            return n < _contents.Length ? GetYAt(n) : double.NaN;
        }

        /// <summary>
        /// Возвращает верхнюю и нижнюю границы, а также содержимое интервала, содержащего указанное число
        /// </summary>
        /// <returns>границы и содержимое, если указанное число хранится в пределах гистограммы,
        /// иначе - null</returns>        
        public double[] GetBinLimitsAndValueByX(double x)
        {
            if (x >= _minimum)
            {
                int index = (int)Math.Floor((x - _minimum) / _binWidth);
                if (index < _contents.Length)
                {
                    double[] result = new double[3];
                    result[0] = _minimum + index * _binWidth;
                    result[1] = result[0] + _binWidth;
                    result[2] = _contents[index];
                    return result;
                }
            }
            return null;
        }

        /// <summary>
        /// Возвращает ширину интервала
        /// </summary>
        public double BinWidth
        {
            get 
            {
                return _binWidth; 
            }
        }

        /// <summary>
        /// Возвращает разницу содержимого между указанными аргументами
        /// </summary>
        /// <param name="left"></param>
        /// <param name="right"></param>
        /// <returns></returns>
        public double GetCountsBetween(double left, double right)
        {
            int n = BinIndex(left);
            int m = BinIndex(right);
            double sum = _contents[n] * ((_minimum - left) / _binWidth - (n + 1))
                        + _contents[m] * ((right - _minimum) / _binWidth - m);
            while (++n < m)
                sum += _contents[n];
            return sum;
        }

        /// <summary>
        /// Возвращает сумму содержимого, расположенного левее указанного аргумента
        /// </summary>
        /// <param name="right"></param>
        /// <returns></returns>
        public double GetSumUpTo(double right)
        {
            int n = BinIndex(right);
            double sum = _contents[n] * ((right - _minimum) / _binWidth - n)
                                                                + _underflow;
            for (int i = 0; i < n; i++)
                sum += _contents[i];
            return sum;
        }

        /// <summary>
        /// Возвращает число интервалов гистограммы
        /// </summary>
        public double Dimension
        {
            get
            {
                if (_cached)
                    FlushCache();
                return _contents.Length;
            }
        }

        /// <summary>
        /// Возвращает верхнюю границу интервала последнего интервала
        /// </summary>
        public double Maximum
        {
            get 
            { 
                return _minimum + (_contents.Length - 1) * _binWidth; 
            }
        }

        /// <summary>
        /// Возвращает нижнюю границу интервала первого интервала
        /// </summary>
        public double Minimum
        {
            get 
            { 
                return _minimum; 
            }
        }

        /// <summary>
        /// Возвращает диапазон значений для выстраивания
        /// </summary>
        /// <returns>
        /// 0: минимум X-диапазона
        /// 1: максимум X-диапазона
        /// 2: минимум Y-диапазона
        /// 3: максимум Y-диапазона
        /// </returns>
        public double[] Range
        {
            get
            {
                if (_cached)
                    FlushCache();
                double[] result = new double[] 
                {  _minimum, 
                    Maximum, 
                    0, 
                    0 };
                for (int n = 0; n < _contents.Length; n++)
                    result[3] = Math.Max(result[3], _contents[n]);
                return result;
            }
        }

        /// <summary>
        /// Возвращает эксцесс значений, накопленных в интервалах гистограммы.
        /// Эксцесс измеряет крутизну уклона распределения в районе максимума
        /// </summary>
        /// <remarks>Эксцесс Нормального распределения всегда равен 0 по определению</remarks>        
        public double Kurtosis
        {
            get
            {
                if (_cached)
                    FlushCache();
                return _moments.Kurtosis;
            }
        }

        /// <summary>
        /// Возвращает статистические моменты
        /// </summary>
        protected StatisticalMomentsFixed Moments
        {
            get 
            {
                return _moments; 
            }
        }

        /// <summary>
        /// Возвращает число значений, накопленных ниже нижнего интервала
        /// </summary>
        public long Overflow
        {
            get 
            { 
                return _cached ? 0 : _overflow; 
            }
        }

        /// <summary>
        /// Сбрасывает гистограмму
        /// </summary>
        public void Reset()
        {
            if (_moments == null)
                _moments = new StatisticalMomentsFixed();
            else
                _moments.Reset();
            _underflow = 0;
            _overflow = 0;
            Array.Clear(_contents, 0, _contents.Length);
        }

        /// <summary>
        /// Позволяет содержимому гистограммы расти, чтобы содержать все накопленные значения.
        /// </summary>
        /// <remarks>
        /// Не следует вызывать после значений, накопленных в областях левее и правее пределов гистограммы;
        /// Кидает InvalidOperationException, если в гистограмме уже есть какие-либо значения        
        /// </remarks>        
        public void SetGrowthAllowed()
        {
            if (_underflow != 0 || _overflow != 0)
            {
                if (!_cached)
                    throw new InvalidOperationException();
            }
            _growthAllowed = true;
        }

        /// <summary>
        /// Устанавливает флаг целочисленной ширины интервалов.
        /// </summary>        
        /// <remarks>
        /// Может быть вызван только если гистограмма закэширована;
        /// Кидает InvalidOperationException, если в гистограмме уже есть какие-либо значения        
        /// </remarks>                
        public void SetIntegerBinWidth()
        {
            if (!_cached)
                throw new InvalidOperationException();
            _integerBinWidth = true;
        }

        /// <summary>
        /// Возвращает число точек в ряду
        /// </summary>
        public int Size
        {
            get
            {
                if (_cached)
                    FlushCache();
                return _contents.Length;
            }
        }

        /// <summary>
        /// Возвращает уровень ассиметрии значений, накопленных в интервалах гистограммы
        /// </summary>
        public double Skewness
        {
            get
            {
                if (_cached)
                    FlushCache();
                return _moments.Skewness;
            }
        }

        /// <summary>
        /// Возвращает стандартное отклонение значений, накопленных в интервалах гистограммы
        /// </summary>
        public double StandardDeviation
        {
            get
            {
                if (_cached)
                    FlushCache();
                return _moments.StandardDeviation;
            }
        }

        /// <summary>
        /// Возвращает уровень доверия по T-тесту с данными, накопленными в указанной гистограмме
        /// </summary>        
        public double GetTConfidenceLevel(Histogram h)
        {
            return _moments.GetTConfidenceLevel(h._moments);
        }

        /// <summary>
        /// Возвращает общее число значений
        /// </summary>
        public long TotalCount
        {
            get
            {
                return _cached ? _underflow : _moments.Count + _overflow + _underflow;
            }
        }

        /// <summary>
        /// Возвращает число значений, хранящихся ниже нижнего интервала
        /// </summary>
        public long Underflow
        {
            get 
            { 
                return _cached ? 0 : _underflow; 
            }
        }

        /// <summary>
        /// Возвращает дисперсию значений, накопленных в интервалах гистограммы
        /// </summary>
        public double Variance
        {
            get
            {
                if (_cached)
                    FlushCache();
                return _moments.Variance;
            }
        }

        /// <summary>
        /// Возвращает взвешенную точку в указанном интервале
        /// </summary>
        /// <param name="needBinIndex"></param>
        /// <returns></returns>
        public WeightedPoint GetWeightedPointAt(int needBinIndex)
        {
            return new WeightedPoint(GetXAt(needBinIndex), _contents[needBinIndex]);
        }

        /// <summary>
        /// Возвращает серидину указанного интервала
        /// </summary>
        /// <param name="needBinIndex"></param>
        /// <returns></returns>
        public double GetXAt(int needBinIndex)
        {
            return (needBinIndex + 0.5) * _binWidth + _minimum;
        }

        /// <summary>
        /// Возвращает значение по индексу
        /// </summary>
        /// <param name="needIndex"></param>
        /// <returns></returns>
        public double GetYAt(int needIndex)
        {
            if (_cached)
                FlushCache();
            return (needIndex >= 0 && needIndex < _contents.Length) ? _contents[needIndex] : 0;
        }
    }
}
